package logs

import (
	"encoding/json"
	"errors"
	"fmt"
	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"io/fs"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
)

var (
	Logger          *zap.Logger
	LoggerSTD       logr.Logger
	FluentBitLogger logr.Logger
	LogFile         *os.File
	JsonEncoder     zapcore.Encoder
	AtomLevel                  = zap.NewAtomicLevel()
	Fs              FileSystem = osFS{}
	LogLevel                   = os.Getenv("LOG_LEVEL")
	LogsPath                   = os.Getenv("LOGS_PATH")
	LogsFileName               = os.Getenv("LOG_FILE_NAME")
	LogsFluentPath             = ""
)

type reverseLogsLevels int

const (
	DebugLevel  = reverseLogsLevels(zap.DebugLevel)
	InfoLevel   = reverseLogsLevels(zap.InfoLevel)
	WarnLevel   = reverseLogsLevels(-1 * zap.WarnLevel)
	ErrorLevel  = reverseLogsLevels(-1 * zap.ErrorLevel)
	DPanicLevel = reverseLogsLevels(-1 * zap.DPanicLevel)
	PanicLevel  = reverseLogsLevels(-1 * zap.PanicLevel)
	FatalLevel  = reverseLogsLevels(-1 * zap.FatalLevel)
)

type outputLogs int

const (
	StdOut outputLogs = iota
	FileOut
	MultiOut
)

// osFS implements fileSystem using the local disk.
type osFS struct{}

func (osFS) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
	return os.OpenFile(name, flag, perm)
}
func (osFS) Stat(name string) (os.FileInfo, error) { return os.Stat(name) }

func (osFS) Remove(name string) error {
	return os.Remove(name)
}

type FileSystem interface {
	OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
	Stat(name string) (os.FileInfo, error)
	Remove(name string) error
}

func parseAndSetReverseLevel(level string) error {

	switch level {
	case "debug", "DEBUG":
		AtomLevel.SetLevel(zapcore.Level(DebugLevel))
	case "info", "INFO", "":
		AtomLevel.SetLevel(zapcore.Level(InfoLevel))
	case "warn", "WARN":
		AtomLevel.SetLevel(zapcore.Level(WarnLevel))
	case "error", "ERROR":
		AtomLevel.SetLevel(zapcore.Level(ErrorLevel))
	case "dpanic", "DPANIC":
		AtomLevel.SetLevel(zapcore.Level(DPanicLevel))
	case "panic", "PANIC":
		AtomLevel.SetLevel(zapcore.Level(PanicLevel))
	case "fatal", "FATAL":
		AtomLevel.SetLevel(zapcore.Level(FatalLevel))
	default:
		AtomLevel.SetLevel(zapcore.Level(FatalLevel))
		return fmt.Errorf("unrecognized level: %q", level)
	}
	return nil
}

func startCatchSignals() {
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		LogFile.Close()
		os.Exit(0)
	}()
}

func createLoggerFile(filePath string, fileName string) (*os.File, error) {

	_, err := Fs.Stat(filePath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			err := os.Mkdir(filePath, os.FileMode(0755))
			if err != nil {
				return &os.File{}, err
			}
		} else {
			return &os.File{}, err
		}
	}

	fullPath := filepath.Join(filePath, fileName)

	f, err := Fs.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0755)
	if err != nil {
		return &os.File{}, err
	}

	return f, nil
}

type LogOptions struct {
	Out          outputLogs
	LvlFluentBit zapcore.Level
	LvlConsole   zapcore.Level
}

func Log(logsOutputOptions LogOptions, keysAndValues ...interface{}) {
	if logsOutputOptions.Out == FileOut || logsOutputOptions.Out == MultiOut {
		FluentBitLogger.V(int(logsOutputOptions.LvlFluentBit)).Info("", keysAndValues...)
	}
	if logsOutputOptions.Out == StdOut || logsOutputOptions.Out == MultiOut {
		data, _ := json.Marshal(keysAndValues)
		Logger.Log(logsOutputOptions.LvlConsole, string(data))
	}
}

func Fatal(keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.FatalLevel}, keysAndValues...)
}

func Error(keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.ErrorLevel}, keysAndValues...)
}

func Errorf(message string, keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.ErrorLevel}, fmt.Sprintf(message, keysAndValues...))
}

func Warn(keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.WarnLevel}, keysAndValues...)
}

func Info(keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.InfoLevel}, keysAndValues...)
}

func Infof(message string, keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.InfoLevel}, fmt.Sprintf(message, keysAndValues...))
}

func Debug(keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.DebugLevel}, keysAndValues...)
}

func Debugf(message string, keysAndValues ...interface{}) {
	Log(LogOptions{Out: StdOut, LvlConsole: zap.DebugLevel}, fmt.Sprintf(message, keysAndValues...))
}

func PersistError(keysAndValues ...interface{}) {
	Log(LogOptions{Out: MultiOut, LvlFluentBit: zap.InfoLevel, LvlConsole: zap.ErrorLevel},
		keysAndValues...)
}

func PersistInfo(keysAndValues ...interface{}) {
	Log(LogOptions{Out: MultiOut, LvlFluentBit: zap.InfoLevel, LvlConsole: zap.InfoLevel},
		keysAndValues...)
}

func PersistDebug(keysAndValues ...interface{}) {
	Log(LogOptions{Out: MultiOut, LvlFluentBit: zap.InfoLevel, LvlConsole: zap.DebugLevel},
		keysAndValues...)
}

func PersistLog(level zapcore.Level, keysAndValues ...interface{}) {
	Log(LogOptions{Out: MultiOut, LvlFluentBit: zap.InfoLevel, LvlConsole: level},
		keysAndValues...)
}

func LoggersInit() {
	var err error

	encoderCfg := zap.NewDevelopmentEncoderConfig()
	encoderCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
	encoderCfg.EncodeTime = zapcore.RFC3339TimeEncoder
	JsonEncoder = zapcore.NewConsoleEncoder(encoderCfg)

	Logger = zap.New(zapcore.NewCore(
		JsonEncoder,
		zapcore.WriteSyncer(os.Stdout),
		AtomLevel))

	LoggerSTD = zapr.NewLogger(Logger)

	defer Logger.Sync()

	if err = parseAndSetReverseLevel(LogLevel); err != nil {
		Log(LogOptions{Out: StdOut, LvlConsole: zap.FatalLevel}, fmt.Sprintf(err.Error()))
		os.Exit(1)
	}

	LogFile, err = createLoggerFile(LogsPath, LogsFileName)
	if err != nil {
		Log(LogOptions{Out: StdOut, LvlConsole: zap.FatalLevel}, zap.FatalLevel, fmt.Sprintf("Error create logger file: %s", err.Error()))
		os.Exit(1)
	}
	startCatchSignals()

	encoderCfgFluentBit := zapcore.EncoderConfig{
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     zapcore.EpochTimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	}

	FluentBitLogger = zapr.NewLogger(zap.New(zapcore.NewCore(
		zapcore.NewJSONEncoder(encoderCfgFluentBit),
		zapcore.WriteSyncer(LogFile),
		zap.NewAtomicLevel())))
}

func ChangeDebugLevelServerStart(addr string, serveMux *http.ServeMux) {
	Infof("Starting logger on addr %s", addr)
	Infof("See log level by path %s", fmt.Sprintf("http://localhost%s/logging", addr))
	serveMux.Handle("/logging", AtomLevel)
}

func IsDebugMode() bool {
	return AtomLevel.Level() == zap.DebugLevel
}
